/* NOTICE
 
    Jim - the Java Image Manager, copyright (c) 2005 - 2007 Grant Gardner 
 
    Jim is free software: you can redistribute it and/or modify
    it under the terms of the GNU General Public License as published by
    the Free Software Foundation, either version 3 of the License, or
    (at your option) any later version.

    Jim is distributed in the hope that it will be useful,
    but WITHOUT ANY WARRANTY; without even the implied warranty of
    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
    GNU General Public License for more details.

    You should have received a copy of the GNU General Public License
    along with this program.  If not, see <http://www.gnu.org/licenses/>.
  
    In addition, as a special exception, you are permitted to link this code
    with the 3rd party libraries required to compile this file and to
    distribute a linked combination including them. You must obey the
    GNU General Public License in all respects for all of the code except for
    those 3rd party libraries that are licenced under terms incompatible with the
    GNU General Public License.  
 
 
 NOTICE */

package au.com.lastweekend.jim.io.metadata;

import java.lang.reflect.Array;
import java.text.DateFormat;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;

import au.com.lastweekend.jim.util.Rational;

/**
 * @author <a href="mailto:grant@lastweekend.com.au"> <Grant Gardner> </a>
 * @version $Id: Directory.java,v 1.3 2006/02/20 12:21:10 ggardner Exp $
 */
public abstract class Directory {

    private Directory _linkedDirectory;

    private Directory _parentDirectory;

    private final HashMap<Integer, Object> _tagMap;

    private AbstractTagDescriptor _descriptor;

    private final List<Tag> _definedTagList;

    private List<String> _errorList;

    // ABSTRACT METHODS

    private List<Directory> _subDirectories = new ArrayList<Directory>();

    public abstract String getName();

    protected abstract Map<Integer, String> getTagNameMap();

    // CONSTRUCTORS

    public Directory(Directory parent) {

        _tagMap = new LinkedHashMap<Integer, Object>();
        _definedTagList = new ArrayList<Tag>();
        _errorList = new ArrayList<String>();
        _parentDirectory = parent;

    }

    // VARIOUS METHODS

    public boolean isRootDirectory() {

        return _parentDirectory == null;
    }

    public boolean containsTag(int tagType) {

        return _tagMap.containsKey(tagType);
    }

    public Iterator<Tag> getTagIterator() {

        return _definedTagList.iterator();
    }

    public int getTagCount() {

        return _definedTagList.size();
    }

    public void setDescriptor(AbstractTagDescriptor descriptor) {

        if (descriptor == null) {
            throw new IllegalArgumentException("cannot set a null descriptor");
        }
        _descriptor = descriptor;
    }

    public void addError(String message) {

        if (_errorList == null) {
            _errorList = new ArrayList<String>();

            _errorList.add(message);
        }
    }

    public boolean hasErrors() {

        return (_errorList != null && _errorList.size() > 0);
    }

    public Iterator<String> getErrors() {

        return _errorList.iterator();
    }

    public int getErrorCount() {

        return _errorList.size();
    }

    public void setTag(int tagType, int formatCode, Object value) {

        if (value == null) {
            throw new IllegalArgumentException("cannot set a null object for tagType " + tagType);
        }

        Integer key = tagType;
        if (!_tagMap.containsKey(key)) {
            _definedTagList.add(new Tag(tagType, formatCode, this));
        }
        _tagMap.put(key, value);
    }

    public void removeTag(int tagType) {

        _tagMap.remove((tagType));
        Iterator<Tag> iter = _definedTagList.iterator();
        while (iter.hasNext()) {
            Tag currentTag = iter.next();
            if (currentTag.getTagType() == tagType) {
                iter.remove();
            }
        }
    }

    // TAG GETTERS

    public int getInt(int tagType) throws MetadataException {

        Object o = getObject(tagType);
        if (o == null) {
            throw new MetadataException("Tag " + getTagName(tagType) + " has not been set -- check using containsTag() first");
        } else if (o instanceof String) {
            try {
                return Integer.parseInt((String) o);
            } catch (NumberFormatException nfe) {
                // convert the char array to an int
                String s = (String) o;
                int val = 0;
                for (int i = s.length() - 1; i >= 0; i--) {
                    val += s.charAt(i) << (i * 8);
                }
                return val;
            }
        } else if (o instanceof Number) {
            return ((Number) o).intValue();
        }
        throw new MetadataException("Requested tag cannot be cast to int");
    }

    public double getDouble(int tagType) throws MetadataException {

        Object o = getObject(tagType);
        if (o == null) {
            throw new MetadataException("Tag " + getTagName(tagType) + " has not been set -- check using containsTag() first");
        } else if (o instanceof String) {
            try {
                return Double.parseDouble((String) o);
            } catch (NumberFormatException nfe) {
                throw new MetadataException("unable to parse string " + o + " as a double", nfe);
            }
        } else if (o instanceof Number) {
            return ((Number) o).doubleValue();
        }
        throw new MetadataException("Requested tag cannot be cast to double");
    }

    public float getFloat(int tagType) throws MetadataException {

        Object o = getObject(tagType);
        if (o == null) {
            throw new MetadataException("Tag " + getTagName(tagType) + " has not been set -- check using containsTag() first");
        } else if (o instanceof String) {
            try {
                return Float.parseFloat((String) o);
            } catch (NumberFormatException nfe) {
                throw new MetadataException("unable to parse string " + o + " as a float", nfe);
            }
        } else if (o instanceof Number) {
            return ((Number) o).floatValue();
        }
        throw new MetadataException("Requested tag cannot be cast to float");
    }

    public long getLong(int tagType) throws MetadataException {

        Object o = getObject(tagType);
        if (o == null) {
            throw new MetadataException("Tag " + getTagName(tagType) + " has not been set -- check using containsTag() first");
        } else if (o instanceof String) {
            try {
                return Long.parseLong((String) o);
            } catch (NumberFormatException nfe) {
                throw new MetadataException("unable to parse string " + o + " as a long", nfe);
            }
        } else if (o instanceof Number) {
            return ((Number) o).longValue();
        }
        throw new MetadataException("Requested tag cannot be cast to long");
    }

    public boolean getBoolean(int tagType) throws MetadataException {

        Object o = getObject(tagType);
        if (o == null) {
            throw new MetadataException("Tag " + getTagName(tagType) + " has not been set -- check using containsTag() first");
        } else if (o instanceof Boolean) {
            return ((Boolean) o).booleanValue();
        } else if (o instanceof String) {
            try {
                return Boolean.getBoolean((String) o);
            } catch (NumberFormatException nfe) {
                throw new MetadataException("unable to parse string " + o + " as a boolean", nfe);
            }
        } else if (o instanceof Number) {
            return (((Number) o).doubleValue() != 0);
        }
        throw new MetadataException("Requested tag cannot be cast to boolean");
    }

    public java.util.Date getDate(int tagType) throws MetadataException {

        Object o = getObject(tagType);
        if (o == null) {
            throw new MetadataException("Tag " + getTagName(tagType) + " has not been set -- check using containsTag() first");
        } else if (o instanceof java.util.Date) {
            return (java.util.Date) o;
        } else if (o instanceof String) {
            // add new dateformat strings to make this method even smarter
            // so far, this seems to cover all known date strings
            // (for example, AM and PM strings are not supported...)
            String[] datePatterns = {"yyyy:MM:dd HH:mm:ss", "yyyy:MM:dd HH:mm", "yyyy-MM-dd HH:mm:ss", "yyyy-MM-dd HH:mm"};
            String dateString = (String) o;
            for (int i = 0; i < datePatterns.length; i++) {
                try {
                    DateFormat parser = new java.text.SimpleDateFormat(datePatterns[i]);
                    return parser.parse(dateString);
                } catch (java.text.ParseException ex) {
                    // simply try the next pattern
                }
            }
        }
        throw new MetadataException("Requested tag cannot be cast to java.util.Date");
    }

    public String[] getStringArray(int tagType) throws MetadataException {

        Object o = getObject(tagType);
        if (o == null) {
            throw new MetadataException("Tag " + getTagName(tagType) + " has not been set -- check using containsTag() first");
        } else if (o instanceof String[]) {
            return (String[]) o;
        } else if (o instanceof String) {
            String[] strings = {(String) o};
            return strings;
        } else if (o instanceof int[]) {
            int[] ints = (int[]) o;
            String[] strings = new String[ints.length];
            for (int i = 0; i < strings.length; i++) {
                strings[i] = Integer.toString(ints[i]);
            }
            return strings;
        } else if (o instanceof byte[]) {
            byte[] bytes = (byte[]) o;
            String[] strings = new String[bytes.length];
            for (int i = 0; i < strings.length; i++) {
                strings[i] = Byte.toString(bytes[i]);
            }
            return strings;
        }
        throw new MetadataException("Requested tag cannot be cast to String array (" + o.getClass().toString() + ")");
    }

    public int[] getIntArray(int tagType) throws MetadataException {

        Object o = getObject(tagType);
        if (o == null) {
            throw new MetadataException("Tag " + getTagName(tagType) + " has not been set -- check using containsTag() first");
        } else if (o instanceof int[]) {
            return (int[]) o;
        } else if (o instanceof byte[]) {
            byte[] bytes = (byte[]) o;
            int[] ints = new int[bytes.length];
            for (int i = 0; i < bytes.length; i++) {
                byte b = bytes[i];
                ints[i] = b;
            }
            return ints;
        } else if (o instanceof String) {
            String str = (String) o;
            int[] ints = new int[str.length()];
            for (int i = 0; i < str.length(); i++) {
                ints[i] = str.charAt(i);
            }
            return ints;
        }
        throw new MetadataException("Requested tag cannot be cast to int array (" + o.getClass().toString() + ")");
    }

    public Rational[] getRationalArray(int tagType) throws MetadataException {

        Object o = getObject(tagType);
        if (o == null) {
            throw new MetadataException("Tag " + getTagName(tagType) + " has not been set -- check using containsTag() first");
        }
        throw new MetadataException("Requested tag cannot be cast to Rational array (" + o.getClass().toString() + ")");
    }

    public byte[] getByteArray(int tagType) throws MetadataException {

        Object o = getObject(tagType);
        if (o == null) {
            throw new MetadataException("Tag " + getTagName(tagType) + " has not been set -- check using containsTag() first");
        } else if (o instanceof byte[]) {
            return (byte[]) o;
        } else if (o instanceof int[]) {
            int[] ints = (int[]) o;
            byte[] bytes = new byte[ints.length];
            for (int i = 0; i < ints.length; i++) {
                bytes[i] = (byte) ints[i];
            }
            return bytes;
        } else if (o instanceof String) {
            String str = (String) o;
            byte[] bytes = new byte[str.length()];
            for (int i = 0; i < str.length(); i++) {
                bytes[i] = (byte) str.charAt(i);
            }
            return bytes;
        }
        throw new MetadataException("Requested tag cannot be cast to byte array (" + o.getClass().toString() + ")");
    }

    public Rational getRational(int tagType) throws MetadataException {

        Object o = getObject(tagType);
        if (o == null) {
            throw new MetadataException("Tag " + getTagName(tagType) + " has not been set -- check using containsTag() first");
        } else if (o instanceof Rational) {
            return (Rational) o;
        }
        throw new MetadataException("Requested tag cannot be cast to Rational");
    }

    public String getString(int tagType) {

        Object o = getObject(tagType);
        if (o == null) {
            return null;
        } else if (o instanceof Rational) {
            return ((Rational) o).toSimpleString(true);
        } else if (o.getClass().isArray()) {
            // handle arrays of objects and primitives
            int arrayLength = Array.getLength(o);
            // determine if this is an array of objects i.e. [Lcom.drew.blah
            boolean isObjectArray = o.getClass().toString().startsWith("class [L");
            StringBuffer sbuffer = new StringBuffer();
            for (int i = 0; i < arrayLength; i++) {
                if (i != 0) {
                    sbuffer.append(' ');
                }
                if (isObjectArray) {
                    sbuffer.append(Array.get(o, i).toString());
                } else {
                    sbuffer.append(Array.getInt(o, i));
                }
            }
            return sbuffer.toString();
        } else {
            return o.toString();
        }
    }

    public Object getObject(int tagType) {

        return _tagMap.get((tagType));
    }

    // OTHER METHODS

    public String getTagName(int tagType) {

        Integer key = (tagType);
        Map<Integer, String> nameMap = getTagNameMap();
        if (!nameMap.containsKey(key)) {
            String hex = Integer.toHexString(tagType);
            while (hex.length() < 4) {
                hex = "0" + hex;
            }
            return "Unknown tag (0x" + hex + ")";
        }
        return nameMap.get(key);
    }

    public String getDescription(int tagType) throws MetadataException {

        if (_descriptor == null) {
            throw new MetadataException("a descriptor must be set using setDescriptor(...) before descriptions can be provided");
        }

        return _descriptor.getDescription(tagType);
    }

    public void addSubDirectory(int tagType, int formatCode, Directory subDirectory) {

        _subDirectories.add(subDirectory);
        setTag(tagType, formatCode, subDirectory);
    }

    public void setLinkedDirectory(Directory linkedDirectory) {

        _linkedDirectory = linkedDirectory;

    }

    public Directory getLinkedDirectory() {

        return _linkedDirectory;
    }

    public List<Directory> getSubDirectories() {

        return _subDirectories;
    }

    public Directory getParentDirectory() {

        return _parentDirectory;
    }

    protected List<Tag> getDefinedTagList() {

        return _definedTagList;
    }

    protected HashMap<Integer, Object> getTagMap() {

        return _tagMap;
    }

    protected AbstractTagDescriptor getDescriptor() {

        return _descriptor;
    }

    public boolean hasLinkedDirectory() {

        return _linkedDirectory != null;
    }

    public boolean isTagUpdateable(int tagType) {

        if (_descriptor != null) {
            return _descriptor.isTagUpdateable(tagType);
        }
        return false;
    }

}